import json
import os
import sys
import shutil
import itertools # 导入itertools用于参数组合
from datetime import datetime, timedelta
from concurrent.futures import ProcessPoolExecutor, as_completed

import pandas as pd
import backtrader as bt
import baostock as bs
from tqdm import tqdm
from pandas.tseries.offsets import BDay

# --- 从新模块中导入组件 ---
from utils import save_report
from data_provider import get_market_data, prepare_stock_data
from signal_generator import generate_buy_signals_with_pandas
from strategy import StockSelectionStrategy, ProgressObserver
# -------------------------

def create_empty_report(momentum_threshold, max_pullback_pct, trailing_stop_pct, trade_size, initial_cash, note):
    """
    创建一个空的回测报告，用于跳过回测的情况
    
    :param momentum_threshold: 动量阈值
    :param max_pullback_pct: 最大回调百分比
    :param trailing_stop_pct: 跟踪止损百分比
    :param trade_size: 交易股数
    :param initial_cash: 初始资金
    :param note: 备注信息
    :return: 包含空回测结果的字典
    """
    return {
        'momentum_threshold': momentum_threshold,
        'max_pullback_pct': max_pullback_pct,
        'trailing_stop_pct': trailing_stop_pct,
        'trade_size': trade_size,
        'Total Return (%)': 0,
        'Sharpe Ratio': 0,
        'Win Rate (%)': 0,
        'Profit/Loss Ratio': 0,
        'Total Trades': 0,
        'Final Portfolio Value': initial_cash,
        'Note': note
    }

def run_backtest(config, stock_codes, market_df, momentum_threshold, max_pullback_pct, trailing_stop_pct, trade_size, ma_divergence_20_max, ma_divergence_60_max, rsi_overbought_threshold, precise_pullback_max_dist_pct, market_rsi_max):
    """
    执行单次回测的核心逻辑。
    
    :param config: 全局配置字典
    :param stock_codes: 用于生成信号的股票代码列表
    :param market_df: 大盘指数数据
    :param momentum_threshold: 动量阈值 (5日回报)
    :param max_pullback_pct: 最大回调百分比
    :param trailing_stop_pct: 跟踪止损的回撤百分比
    :param trade_size: 每次交易的股数
    :param ma_divergence_20_max: 20日乖离率最大值
    :param ma_divergence_60_max: 60日乖离率最大值
    :param rsi_overbought_threshold: RSI超买阈值
    :param precise_pullback_max_dist_pct: 精准回调与MA10的最大距离
    :param market_rsi_max: 允许入市的大盘RSI上限
    :return: 包含回测结果的字典
    """
    bt_settings = config['backtest_settings']
    output_settings = config['output_settings']
    data_settings = config['data_settings']
    CACHE_DIR = 'feather_cache'

    start_date_obj = pd.to_datetime(bt_settings['start_date'])
    PREHEAT_TRADING_DAYS = 80
    start_date_for_data_obj = start_date_obj - BDay(PREHEAT_TRADING_DAYS)

    # --- 阶段五：使用Pandas生成所有买入信号 ---
    print("\n--- 阶段一：使用Pandas生成买入信号 ---")
    print(f"当前测试参数: momentum_threshold={momentum_threshold}, max_pullback_pct={max_pullback_pct}, trailing_stop_pct={trailing_stop_pct}, trade_size={trade_size}")
    
    buy_signals, hot_stocks = generate_buy_signals_with_pandas(
        all_stock_codes=stock_codes,
        market_df=market_df.copy(),
        cache_dir=CACHE_DIR,
        start_date_str=bt_settings['start_date'],
        end_date_str=bt_settings['end_date'],
        momentum_threshold=momentum_threshold,
        max_pullback_pct=max_pullback_pct,
        max_price=bt_settings.get('max_price', 9999),
        required_start_date_obj=start_date_for_data_obj.date(),
        ma_divergence_20_max=ma_divergence_20_max,
        ma_divergence_60_max=ma_divergence_60_max,
        rsi_overbought_threshold=rsi_overbought_threshold,
        precise_pullback_max_dist_pct=precise_pullback_max_dist_pct,
        market_rsi_max=market_rsi_max
    )

    if not hot_stocks:
        print("\n--- 信号生成阶段未发现任何股票，跳过此次回测。 ---")
        return create_empty_report(
            momentum_threshold, 
            max_pullback_pct, 
            trailing_stop_pct, 
            trade_size, 
            bt_settings['initial_cash'],
            'No signals generated'
        ), 0

    print(f"\n--- 海选完成：共发现 {len(hot_stocks)} 只潜力股，合计产生 {sum(len(v) for v in buy_signals.values())} 个买入信号。 ---")

    # --- 阶段六：使用Backtrader进行精准回测 ---
    print(f"\n--- 阶段二：对海选出的 {len(hot_stocks)} 只股票进行精准回测 ---")
    cerebro = bt.Cerebro(stdstats=False)
    # 设置broker参数，使交易在当天收盘价执行(coc=True)
    cerebro.broker.setcash(bt_settings['initial_cash'])
    cerebro.broker.setcommission(commission=bt_settings['commission_rate'])
    cerebro.broker.set_coc(True)  # 设置为收盘价成交(Close on Close)
    
    strategy_params = {
        'buy_signals': buy_signals,
        'backtest_start_date': bt_settings['start_date'],
        'trailing_stop_pct': trailing_stop_pct,
        'trade_size': trade_size
    }
    # --- 新增：从默认参数中获取长上影线卖出阈值 ---
    strategy_params['sell_on_long_upper_shadow_pct'] = config.get('default_strategy_params', {}).get('sell_on_long_upper_shadow_pct', 0.03)

    cerebro.addstrategy(StockSelectionStrategy, **strategy_params)
    cerebro.addobserver(ProgressObserver)
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer')
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

    print("正在加载大盘数据...")
    market_df_copy = market_df.copy()
    market_df_copy.set_index('date', inplace=True)
    market_data_feed = bt.feeds.PandasData(
        dataname=market_df_copy,
        fromdate=start_date_for_data_obj,
        todate=pd.to_datetime(bt_settings['end_date']),
        name='market_index'
    )
    cerebro.adddata(market_data_feed, name='market_index')

    print(f"正在加载 {len(hot_stocks)} 只股票数据...")
    loaded_stocks = 0
    for symbol in tqdm(hot_stocks, desc="加载股票数据"):
        stock_df_path = os.path.join(CACHE_DIR, f"{symbol}.feather")
        if not os.path.exists(stock_df_path):
            continue
        try:
            stock_df = pd.read_feather(stock_df_path)
            if stock_df.empty or len(stock_df) < 125: 
                continue
            stock_df.set_index('date', inplace=True)
            data_feed = bt.feeds.PandasData(
                dataname=stock_df,
                fromdate=start_date_for_data_obj,
                todate=pd.to_datetime(bt_settings['end_date']),
                name=symbol
            )
            cerebro.adddata(data_feed, name=symbol)
            loaded_stocks += 1
        except Exception:
            continue

    print(f"成功加载 {loaded_stocks} 只股票数据进入回测引擎。")
    if loaded_stocks == 0:
        print("\n--- 未能加载任何股票数据到回测引擎，跳过此次回测。 ---")
        return create_empty_report(
            momentum_threshold, 
            max_pullback_pct, 
            trailing_stop_pct, 
            trade_size, 
            bt_settings['initial_cash'],
            'No stocks loaded'
        ), 0

    results = cerebro.run()
    
    # --- 结果分析 ---
    final_value = cerebro.broker.getvalue()
    initial_value = bt_settings['initial_cash']
    total_return = (final_value - initial_value) / initial_value * 100
    
    analyzer_results = results[0].analyzers
    trade_analysis = analyzer_results.trade_analyzer.get_analysis()
    
    total_trades = trade_analysis.total.total if trade_analysis.total else 0
    win_count = trade_analysis.won.total if trade_analysis.won else 0
    loss_count = trade_analysis.lost.total if trade_analysis.lost else 0
    
    win_rate = (win_count / total_trades * 100) if total_trades > 0 else 0
    
    total_profit = trade_analysis.won.pnl.total if trade_analysis.won.pnl else 0
    total_loss = trade_analysis.lost.pnl.total if trade_analysis.lost.pnl else 0
    
    avg_win = total_profit / win_count if win_count > 0 else 0
    avg_loss = abs(total_loss / loss_count) if loss_count > 0 else 0
    
    profit_loss_ratio = avg_win / avg_loss if avg_loss > 0 else float('inf')
    
    sharpe_ratio = analyzer_results.sharpe_ratio.get_analysis()['sharperatio'] or 0

    report = {
        'momentum_threshold': momentum_threshold,
        'max_pullback_pct': max_pullback_pct,
        'trailing_stop_pct': trailing_stop_pct,
        'trade_size': trade_size,
        'Total Return (%)': round(total_return, 2),
        'Sharpe Ratio': round(sharpe_ratio, 3),
        'Win Rate (%)': round(win_rate, 2),
        'Profit/Loss Ratio': round(profit_loss_ratio, 2),
        'Total Trades': total_trades,
        'Final Portfolio Value': round(final_value, 2),
        'Note': 'Success'
    }
    
    # 在单次回测模式下，保存详细的交易日志
    if output_settings.get('save_trade_log', False) and not config.get('optimization_params', {}).get('enabled', False):
        if hasattr(results[0], 'trade_log') and results[0].trade_log:
            trade_df = pd.DataFrame(results[0].trade_log)
            # 使用新的通用保存函数
            save_report(trade_df, output_settings['trade_log_path'])
            # 打印消息由save_report函数内部处理
        else:
            print("未生成任何交易，故不保存交易日志。")

    return report, len(hot_stocks) if 'hot_stocks' in locals() else 0


if __name__ == '__main__':
    # --- 阶段一：配置与环境准备 ---
    with open('config.json', 'r', encoding='utf-8') as f:
        config = json.load(f)

    bt_settings = config['backtest_settings']
    output_settings = config['output_settings']
    data_settings = config['data_settings']
    execution_mode = data_settings.get('execution_mode', '顺序执行') # 默认使用顺序执行
    opt_params = config.get('optimization_params', {})
    default_strategy_params = config.get('default_strategy_params', {})
    debug_settings = config.get('debug_settings', {})
    CACHE_DIR = 'feather_cache'

    if data_settings.get('force_recache', False):
        print("\n--- 检测到 force_recache=true，正在清理缓存 ---")
        if os.path.exists(CACHE_DIR):
            try:
                shutil.rmtree(CACHE_DIR)
                print(f"缓存目录 '{CACHE_DIR}' 已被成功删除。")
            except OSError as e:
                print(f"错误：删除缓存目录 '{CACHE_DIR}' 失败: {e}")
        else:
            print(f"缓存目录 '{CACHE_DIR}' 不存在，无需清理。")
        
    if not os.path.exists(CACHE_DIR):
        os.makedirs(CACHE_DIR)

    # --- 阶段二：登录Baostock并获取全市场股票列表 ---
    print("\n--- 登录Baostock以准备所有数据 ---")
    lg = bs.login()
    if lg.error_code != '0':
        print(f"Baostock登录失败: {lg.error_msg}")
        sys.exit(1)

    try:
        market_index_symbol = data_settings.get('market_index_symbol', 'sh.000300')
        adjust_flag = data_settings.get('adjust_flag', '1') # 从配置读取复权标志
        print(f"\n--- 正在准备大盘基准数据 ({market_index_symbol}) ---")
        market_df = get_market_data(CACHE_DIR, symbol=market_index_symbol, adjust_flag=adjust_flag)
        if market_df is None:
            sys.exit(1)

        print("\n--- 正在从Baostock获取最新交易状态正常的A股股票列表 ---")
        stock_codes = []
        
        print("正在查询最近一个交易日...")
        today = datetime.today().date()
        rs_trade_dates = bs.query_trade_dates(start_date=(today - timedelta(days=30)).strftime('%Y-%m-%d'),
                                              end_date=today.strftime('%Y-%m-%d'))
        if rs_trade_dates.error_code != '0':
            print(f"错误: 查询交易日失败! Baostock返回: {rs_trade_dates.error_msg}")
            sys.exit(1)

        trade_dates = []
        while (rs_trade_dates.error_code == '0') & rs_trade_dates.next():
             row = rs_trade_dates.get_row_data()
             if row[1] == '1': # 关键修复：只添加is_trading_day='1'的日期
                 trade_dates.append(row[0])
        
        if not trade_dates:
            print("错误: 在最近30天内未能找到任何交易日，请检查Baostock连接。")
            sys.exit(1)

        # 核心逻辑修正：处理盘前运行的情况
        # 如果从API获取的最新交易日是今天，并且我们有至少两个交易日的数据，
        # 那么就选用倒数第二个交易日，以确保获取的是已收盘的数据。
        latest_trading_day = trade_dates[-1]
        if latest_trading_day == today.strftime('%Y-%m-%d') and len(trade_dates) > 1:
            latest_trading_day = trade_dates[-2]
        
        print(f"已自动校准到最近交易日: {latest_trading_day}")

        rs = bs.query_all_stock(day=latest_trading_day)
        if rs.error_code != '0':
            print(f"错误: 查询所有股票列表失败! Baostock返回: {rs.error_msg}")
            sys.exit(1)
        
        # 从配置中读取需要包含的股票前缀，默认为沪深主板
        included_prefixes = tuple(data_settings.get('included_prefixes', ['sh.60', 'sz.00']))
        
        stock_codes = []
        while (rs.error_code == '0') & rs.next():
            info = rs.get_row_data()
            # 根据配置的前缀列表进行动态筛选
            if len(info) >= 3 and info[2] and info[1] == '1' and info[0].startswith(included_prefixes):
                stock_codes.append(info[0].split('.')[1])
        
        if not stock_codes:
            print("错误：未能在最近交易日获取任何符合条件的股票代码，程序终止。")
            sys.exit(1)
            
        print(f"获取到 {len(stock_codes)} 只股票准备进行下一步处理。")

        # --- 阶段三：根据配置过滤股票池 (例如，限定为指数成分股) ---
        index_symbol_to_filter = data_settings.get('index_constituent_symbol')

        if index_symbol_to_filter:
            # 创建一个从符号到查询函数的映射
            query_map = {
                'sh50': bs.query_sz50_stocks,
                'hs300': bs.query_hs300_stocks,
                'zz500': bs.query_zz500_stocks,
                # 注意：Baostock似乎没有直接的中证800或1000接口，这里仅作示例
                # 'zz800': bs.query_zz800_stocks, 
                # 'zz1000': bs.query_zz1000_stocks,
            }

            query_func = query_map.get(index_symbol_to_filter)
            
            if query_func:
                print(f"\n--- 正在根据配置 '{index_symbol_to_filter}' 获取成分股列表 ---")
                constituent_stocks = []
                rs_index = query_func(date=latest_trading_day) # 修复：参数名从 day 改为 date
                if rs_index.error_code != '0':
                    print(f"警告: 查询 '{index_symbol_to_filter}' 成分股失败: {rs_index.error_msg}，将继续使用原股票池。")
                else:
                    while (rs_index.error_code == '0') & rs_index.next():
                        constituent_stocks.append(rs_index.get_row_data()[1].split('.')[1])
                    
                    if constituent_stocks:
                        original_count = len(stock_codes)
                        stock_codes = sorted(list(set(stock_codes) & set(constituent_stocks)))
                        print(f"已将股票池限定为 '{index_symbol_to_filter}' 成分股，数量从 {original_count} 只过滤到 {len(stock_codes)} 只。")
                    else:
                        print(f"警告：未能获取到 '{index_symbol_to_filter}' 成分股列表，将继续使用原股票池。")
            else:
                print(f"警告：在config.json中配置的 'index_constituent_symbol' ('{index_symbol_to_filter}') 不是一个受支持的符号。将使用全市场股票。")
        # --- 过滤结束 ---

        if debug_settings.get('enabled', False):
            debug_stock = debug_settings.get('debug_stock')
            if not debug_stock:
                print("错误：调试模式已开启，但未在config.json中指定 'debug_stock'。")
                sys.exit(1)
            print(f"\n{'='*50}\n  警告: 已进入单股调试模式，将仅回测: {debug_stock}\n{'='*50}\n")
            stock_codes = [debug_stock]

        print("\n--- 正在进行板块过滤 ---")
        excluded_prefixes = tuple(bt_settings.get('excluded_prefixes', []))
        if excluded_prefixes:
            original_count = len(stock_codes)
            stock_codes = [code for code in stock_codes if not code.startswith(excluded_prefixes)]
            print(f"板块过滤前: {original_count} 只, 过滤后: {len(stock_codes)} 只")

        # --- 阶段四：准备回测周期及下载数据 ---
        print(f"\n--- 准备回测数据：共计 {len(stock_codes)} 只股票 ---")
        PREHEAT_TRADING_DAYS = 80 # 为指标计算设定预热期
        start_date_for_data = (pd.to_datetime(bt_settings['start_date']) - BDay(PREHEAT_TRADING_DAYS)).strftime('%Y-%m-%d')
        end_date_for_data = pd.to_datetime(bt_settings['end_date']).strftime('%Y-%m-%d')

        if execution_mode == '顺序执行':
            for code in tqdm(stock_codes, desc="准备数据"):
                prepare_stock_data(code, start_date_for_data, end_date_for_data, CACHE_DIR, adjust_flag)
        elif execution_mode == '并行加速':
            with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
                futures = [
                    executor.submit(
                        prepare_stock_data,
                        code,
                        start_date_for_data, # 使用包含预热期的开始日期
                        end_date_for_data,   # 使用回测结束日期
                        CACHE_DIR,
                        adjust_flag          # 传递复权标志
                    ) for code in stock_codes
                ]
                for future in tqdm(as_completed(futures), total=len(stock_codes), desc="准备股票数据"):
                    future.result() # 等待任务完成，处理可能出现的异常

    except (ValueError, TypeError):
        print(f"错误：无法解析config.json中的日期: start_date='{bt_settings['start_date']}', end_date='{bt_settings['end_date']}'")
        print("请确保日期格式为 YYYY-MM-DD 或 YYYYMMDD。")
        sys.exit(1)
    finally:
        bs.logout()
        print("\nBaostock 已登出。")

    # --- 优化或单次回测执行 ---
    if opt_params.get('enabled', False):
        print("\n--- 检测到 optimization_params.enabled=true，进入多参数组合优化模式 ---")
        
        # 从优化配置中读取参数列表
        momentum_thresholds = opt_params.get('momentum_thresholds', [default_strategy_params.get('momentum_threshold')])
        max_pullback_pcts = opt_params.get('max_pullback_pcts', [default_strategy_params.get('max_pullback_pct')])
        trailing_stop_pcts = opt_params.get('trailing_stop_pcts', [default_strategy_params.get('trailing_stop_pct')])
        trade_sizes = opt_params.get('trade_sizes', [default_strategy_params.get('trade_size')])
        
        # 新增：为优化模式也读取乖离率参数，如果未定义则使用默认值
        ma_divergence_20_max_list = opt_params.get('ma_divergence_20_max_list', [default_strategy_params.get('ma_divergence_20_max')])
        ma_divergence_60_max_list = opt_params.get('ma_divergence_60_max_list', [default_strategy_params.get('ma_divergence_60_max')])
        # 新增：为优化模式读取RSI阈值列表
        rsi_overbought_thresholds_list = opt_params.get('rsi_overbought_thresholds_list', [default_strategy_params.get('rsi_overbought_threshold')])

        param_names = [
            'momentum_thresholds', 'max_pullback_pcts', 'trailing_stop_pcts', 
            'trade_sizes', 'ma_divergence_20_max_list', 'ma_divergence_60_max_list', 
            'rsi_overbought_thresholds_list'
        ]
        # 使用 .get() 安全地获取每个参数列表
        param_values = [opt_params.get(name, [default_strategy_params.get(name.replace('s', '').replace('_list', ''))]) for name in param_names]
        
        # --- 新增参数 ---
        param_values.append(opt_params.get('precise_pullback_max_dist_pcts', [default_strategy_params.get('precise_pullback_max_dist_pct')]))
        param_values.append(opt_params.get('market_rsi_maxs', [default_strategy_params.get('market_rsi_max')]))
        
        # 创建所有参数组合
        param_combinations = list(itertools.product(*param_values))
        
        total_runs = len(param_combinations)
        print(f"共计 {total_runs} 组参数需要优化。")

        all_reports = []
        
        for i, params in enumerate(param_combinations):
            # 动态解包参数，以适应不同数量的优化变量
            p_momentum, p_pullback, p_trailing, p_size, p_ma_20_max, p_ma_60_max, p_rsi_threshold, p_precise_pullback_max_dist_pct, p_market_rsi_max = params
            
            print(f"\n{'='*70}")
            print(f"--- 开始第 {i+1}/{total_runs} 组参数回测 ---")
            
            # 在优化模式下，我们不需要返回的 hot_stocks 数量
            report, _ = run_backtest(
                config=config,
                stock_codes=stock_codes,
                market_df=market_df,
                momentum_threshold=p_momentum,
                max_pullback_pct=p_pullback,
                trailing_stop_pct=p_trailing,
                trade_size=p_size,
                ma_divergence_20_max=p_ma_20_max,
                ma_divergence_60_max=p_ma_60_max,
                rsi_overbought_threshold=p_rsi_threshold,
                precise_pullback_max_dist_pct=p_precise_pullback_max_dist_pct,
                market_rsi_max=p_market_rsi_max
            )
            all_reports.append(report)
            print(f"--- 第 {i+1}/{total_runs} 组参数回测完成 ---")
            print(f"结果: {report}")
            print(f"{'='*70}\n")
            
        # --- 优化结果分析与保存 ---
        print("\n--- 所有参数优化回测完成，正在生成报告 ---")
        results_df = pd.DataFrame(all_reports)
        
        # 使用新的通用保存函数
        report_path = opt_params.get('results_path', 'optimization_results.xlsx')
        save_report(results_df, report_path)
        # 打印消息由save_report函数内部处理
        
        print("\n--- 优化结果摘要 ---")
        print(results_df.sort_values(by='Total Return (%)', ascending=False).head())

    else:
        # --- 单次回测模式 ---
        print("\n--- 单次回测模式 ---")
        
        # 直接从默认参数中读取
        momentum_threshold = default_strategy_params.get('momentum_threshold', 1.0)
        max_pullback_pct = default_strategy_params.get('max_pullback_pct', -10)
        trailing_stop_pct = default_strategy_params.get('trailing_stop_pct', 0.10)
        trade_size = default_strategy_params.get('trade_size', 100)
        
        # 新增：读取乖离率参数
        ma_divergence_20_max = default_strategy_params.get('ma_divergence_20_max', 0.08)
        ma_divergence_60_max = default_strategy_params.get('ma_divergence_60_max', 0.20)
        # 新增：读取RSI阈值
        rsi_overbought_threshold = default_strategy_params.get('rsi_overbought_threshold', 80)
        # 新增：读取精准回调与MA10的最大距离
        precise_pullback_max_dist_pct = default_strategy_params.get('precise_pullback_max_dist_pct', 0.05)
        # 新增：读取允许入市的大盘RSI上限
        market_rsi_max = default_strategy_params.get('market_rsi_max', 80)

        report, _ = run_backtest(
            config, 
            stock_codes, 
            market_df, 
            momentum_threshold, 
            max_pullback_pct, 
            trailing_stop_pct, 
            trade_size,
            ma_divergence_20_max,
            ma_divergence_60_max,
            rsi_overbought_threshold,
            precise_pullback_max_dist_pct,
            market_rsi_max
        )
        all_reports = [report]

    # --- 阶段八：结果汇总与保存 ---
    if 'all_reports' in locals() and all_reports:
        # 准备要保存的数据
        report_df = pd.DataFrame(all_reports)
        
        # 定义列的中文映射
        column_mapping = {
            'momentum_threshold': '动量阈值',
            'max_pullback_pct': '最大回撤%',
            'trailing_stop_pct': '跟踪止损%',
            'trade_size': '交易股数',
            'Total Return (%)': '总回报(%)',
            'Sharpe Ratio': '夏普比率',
            'Win Rate (%)': '胜率(%)',
            'Profit/Loss Ratio': '盈亏比',
            'Total Trades': '总交易数',
            'Final Portfolio Value': '最终市值',
            'Note': '备注'
        }
        report_df.rename(columns=column_mapping, inplace=True)

        # 构建报告的元数据
        meta_data = {
            '回测时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            '股票池': f"{data_settings['index_constituent_symbol']} (海选出: {len(stock_codes)} 只)",
            '开始日期': bt_settings['start_date'],
            '结束日期': bt_settings['end_date']
        }
        meta_df = pd.DataFrame(meta_data, index=[0])

        # 合并元数据和报告数据
        full_report_df = pd.concat([meta_df, report_df], axis=1)

        # 保存报告
        report_path = output_settings['report_path']
        try:
            # 检查文件是否存在以及是否是CSV，以决定是追加还是覆盖
            if os.path.exists(report_path) and report_path.endswith('.csv'):
                # 追加模式：不写入表头
                full_report_df.to_csv(report_path, mode='a', header=False, index=False, encoding='utf-8-sig')
                print(f"报告已成功追加到: {report_path}")
            else:
                # 覆盖模式：使用我们通用的 save_report 函数
                # 它会处理xlsx的格式化和csv的正常保存
                save_report(full_report_df, report_path)

        except Exception as e:
            print(f"保存报告文件 '{report_path}' 时发生错误: {e}")
            
        # --- (新增) 将最终报告打印到控制台 ---
        print("-" * 26) # 打印分隔线
        for index, row in full_report_df.iterrows():
            for col_name, value in row.items():
                print(f"{col_name}: {value}")
        print("-" * 26) # 打印分隔线

    print("\n--- 所有任务执行完毕 ---")